package com.moming.douapisdk.client;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.moming.douapisdk.ApiException;
import com.moming.douapisdk.ApiRuntimeException;
import com.moming.douapisdk.BaseDouYinResponse;
import com.moming.douapisdk.DouYinRequest;
import com.moming.douapisdk.constant.Constants;
import com.moming.douapisdk.constant.DouYinErrorCodeEnum;
import com.moming.douapisdk.internal.dto.RequestDTO;
import com.moming.douapisdk.internal.util.*;
import com.moming.douapisdk.request.oauth.TokenCreateRequest;
import com.moming.douapisdk.request.oauth.TokenRefreshRequest;
import com.moming.douapisdk.response.OauthResponse;
import com.moming.douapisdk.storage.IDouYinConfigStorage;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.*;

/**
 * 工具型应用-抖音API调用客户端
 * <p/>
 * 工具型应用调用 获取token，必须有授权码。且调用API需要传递token
 * <p/>
 * 如果是 自研型应用，请参考{@link SelfUseDouYinClient}
 *
 * @author tianzong
 * @date 2020/7/21
 */
@Data
@NoArgsConstructor
@Slf4j
public class DefaultDouYinClient implements DouYinClient {

    protected String apiUrl;
    protected String appKey;
    protected String appSecret;

    private String version = "2";

    /**
     * 重试间隔
     */
    private int retrySleepMillis = 1000;
    /**
     * 最大重试次数
     */
    private int maxRetryTimes = 5;

    protected IDouYinConfigStorage configStorage;

    private String accessToken;


    public DefaultDouYinClient(IDouYinConfigStorage configStorage) {
        this.configStorage = configStorage;
        this.appKey = this.configStorage.getAppKey();
        this.appSecret = this.configStorage.getAppSecret();
        this.apiUrl = this.configStorage.getApiUrl();
    }

    @Override
    public String getAccessToken() {
        return getAccessToken(false);
    }

    /**
     * 获取accessToken
     *
     * @param forceRefresh 是否强制刷新
     * @return String
     */
    @Override
    public String getAccessToken(boolean forceRefresh) {
        if (StringUtils.isEmpty(this.accessToken)) {
            throw new ApiRuntimeException("token不能为空");
        }
        return this.accessToken;
    }

    /**
     * 执行隐私API请求。
     *
     * @param request 具体的API响应类
     * @return 具体的API响应
     * @throws ApiException API调用异常
     */
    @Override
    public <T extends BaseDouYinResponse> T execute(DouYinRequest<T> request) throws ApiException {

        throw new ApiException( 1000, "非自研型应用需要传accessToken");
    }

    /**
     * 执行API请求。
     *
     * @param request     具体的API响应类
     * @param accessToken 用户授权码
     * @return 具体的API响应
     * @throws ApiException API调用异常
     */
    @Override
    public <T extends BaseDouYinResponse> T execute(DouYinRequest<T> request, String accessToken) throws ApiException {
        setAccessToken(accessToken);
        return executeWrapper(request);
    }

    protected  <T extends BaseDouYinResponse> T executeWrapper(DouYinRequest<T> request) throws ApiException {
        // 异常重试机制
        int retryTimes = 0;
        do {
            try {
                return executeInternal(request);
            } catch (ApiException e) {
                if (retryTimes +1 > this.maxRetryTimes) {
                    log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
                    //最后一次重试失败后，直接抛出异常，不再等待
                    throw new ApiRuntimeException("抖音服务端异常，超出重试次数");
                }

                if (Constants.VISIT_TOO_MANY_ERROR_CODES.contains(e.getCode())) {
                    int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
                    try {
                        log.debug("抖音系统繁忙，{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
                        Thread.sleep(sleepMillis);
                    } catch (InterruptedException e1) {
                        Thread.currentThread().interrupt();
                    }
                } else {
                    throw e;
                }

            }
        } while (retryTimes++ < this.maxRetryTimes);

        log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
        throw new ApiRuntimeException("抖音服务端异常，超出重试次数");
    }

    /**
     * 工具型应用授权
     * code换token
     *
     *
     * @link https://op.jinritemai.com/docs/guide-docs/9/22
     * @param code code授权码
     * @return 获取token
     */
    public OauthResponse code2Access(String code) throws ApiException {
        return code2Access(code, null);
    }

    /**
     * 工具型应用授权
     * @param code 授权码
     * @param shopId 店铺id
     * @return 授权结果
     * @throws ApiException api异常
     */
    public OauthResponse code2Access(String code, String shopId) throws ApiException {
        TokenCreateRequest request = new TokenCreateRequest();
        request.setCode(null != code ? code : "0");
        request.setGrantType(code == null
                ? TokenCreateRequest.GrantTypeEnum.AUTHORIZATION_SELF
                : TokenCreateRequest.GrantTypeEnum.AUTHORIZATION_CODE);
        if (shopId != null) {
            request.setShopId(shopId);
        }

        OauthResponse oauthResponse;
        try {
            oauthResponse = execute(request);
        } catch (Exception e) {
            throw new ApiException(e);
        }
        return oauthResponse;
    }

    /**
     *  刷新AccessToken
     *
     * @param refreshToken 刷新token
     * @return 新的oauth信息
     * @throws ApiException api结果
     */
    public OauthResponse refreshAccessToken(String refreshToken) throws ApiException {
        Objects.requireNonNull(refreshToken);
        TokenRefreshRequest request = new TokenRefreshRequest();
        request.setRefreshToken(refreshToken);
        OauthResponse oauthResponse;
        try {
            oauthResponse = execute(request);
            if (oauthResponse == null) {
                oauthResponse = new OauthResponse();
            }
        } catch (Exception e) {
            throw new ApiException(e);
        }
        return oauthResponse;
    }

    /**
     * 执行调用
     *
     * @param request 请求类型
     * @param <T> 具体请求接口
     * @return 返回数据
     * @throws ApiException api调用异常
     */
    private <T extends BaseDouYinResponse> T executeInternal(DouYinRequest<T> request)
            throws ApiException {

        // 如果是获取token的接口，不用获取token
        String accessToken = "";

        if (!Arrays.asList(Constants.AUTHORIZATION_PATH, Constants.REFRESH_TOKEN_PATH).contains(request.getApiUrl())) {
            accessToken = getAccessToken();
        }

        T tsr = null;

        // 解析结果
        try {
            // 调用api
            RequestParametersHolder requestHolder = invokeApi(request, accessToken);
            tsr = parseBody(requestHolder, request);

        } catch (ApiException e) {

            // 处理token过期问题
            if (Constants.ACCESS_TOKEN_ERROR_CODES.contains(e.getCode())) {
                // 设置过期
                this.configStorage.expireAccessToken();

                // 自动刷新
                if (this.configStorage.autoRefreshToken()) {
                    log.warn("即将重新获取新的access_token，错误代码：{}，错误信息：{}", e.getErrNo(), e.getMessage());
                    return this.executeWrapper(request);
                }
            }

            // 抛出异常
            throw e;

        }

        return tsr;
    }

    /**
     * 解析接口返回数据
     * @param requestHolder 请求参数
     * @param request 请求数据
     * @param <T> 返回对象
     * @return 结果
     */
    private <T extends BaseDouYinResponse> T parseBody(RequestParametersHolder requestHolder, DouYinRequest<T> request)
            throws ApiException {

        JSONObject bodyJsonObject = JSON.parseObject(requestHolder.getResponseBody());

        // 异常检测
        Integer code = bodyJsonObject.getInteger("code");
        if (!code.equals(DouYinErrorCodeEnum.CODE_10000.getCode())) {
            String msg = bodyJsonObject.getString("msg");
            String subCode = bodyJsonObject.getString("sub_code");
            String subMsg = bodyJsonObject.getString("sub_msg");
            String logId = bodyJsonObject.getString("log_id");
            log.error("调用api接口发生错误，错误{}, 请求体--{}",
                    bodyJsonObject,
                    requestHolder);
            throw new ApiException(code, msg, subCode, subMsg, logId);
        }

        // 兼容某些接口，直接返回data就是一个数组
        if (request.isArrayResponseData()) {
            return JSON.parseObject(requestHolder.getResponseBody(), request.getResponseClass());
        }
        return bodyJsonObject.getObject("data", request.getResponseClass());
    }

    private <T extends BaseDouYinResponse> RequestParametersHolder invokeApi(
            DouYinRequest<T> request, String session)
            throws ApiException {
        RequestDTO requestDTO = new RequestDTO();
        requestDTO.setTextParams(request.getTextParams());
        requestDTO.setApiMethodName(request.getApiMethodName());
        requestDTO.setApiUrl(request.getApiUrl());
        return invokeApiInternal(requestDTO, session);
    }

    protected RequestParametersHolder invokeApiInternal(RequestDTO request, String accessToken)
            throws ApiException {

        RequestParametersHolder requestHolder = new RequestParametersHolder();
        if (request.getTextParams() == null) {
            request.setTextParams(new HashMap<>(0));
        }
        DouYinHashMap appParams = new DouYinHashMap(request.getTextParams());
        requestHolder.setParamJson(appParams);


        // 添加协议级请求参数
        DouYinHashMap protocolParams = new DouYinHashMap();
        protocolParams.put(Constants.METHOD, request.getApiMethodName());
        protocolParams.put(Constants.VERSION, request.getApiVersion() != null ? request.getApiVersion() : version);
        if (appKey != null) {
            protocolParams.put(Constants.APP_KEY, appKey);
        }
        Long timestamp = request.getTimestamp();
        if (timestamp == null) {
            timestamp = System.currentTimeMillis();
        }

        protocolParams.put(Constants.TIMESTAMP, new Date(timestamp));
        protocolParams.put(Constants.PARAM_JSON, requestHolder.getParamJson());
        requestHolder.setProtocolParams(protocolParams);

        // 添加签名
        protocolParams.put(Constants.SIGN, DouYinUtils.signRequest(requestHolder, this.appSecret));

        // 签名不算access_token
        if (StringUtils.isNotEmpty(accessToken)) {
            protocolParams.put(Constants.ACCESS_TOKEN, accessToken);
        }
        try {
            // 调用httpClient处理
            String url = buildApiUrl(request.getApiUrl(), requestHolder.getProtocolParams());
            requestHolder.setRequestUrl(url);
            String body = HttpClientHelper.httpGet(url);
            log.debug(" 请求抖音接口url : {} \r\n 返回数据：{}", url, body);
            requestHolder.setResponseBody(body);

        } catch (Exception e) {
            log.error("调用api接口发生异常\r\n Url---->{}\r\n", requestHolder.getRequestUrl(), e);
            throw new ApiException(e);
        }
        return requestHolder;
    }

    /**
     * 组合请求api
     *
     * @param apiUrl api地址
     * @param params 参数
     * @return url
     * @throws IOException io异常
     */
    private String buildApiUrl(String apiUrl, Map<String, String> params) throws IOException {
        return this.apiUrl
                + apiUrl
                + "?"
                + HttpClientHelper.buildQuery(params, Constants.CHARSET_UTF8);
    }

}
